#region dCPL Version 1.1.1
/*
The contents of this file are subject to the dashCommerce Public License
Version 1.1.1 (the "License"); you may not use this file except in
compliance with the License. You may obtain a copy of the License at
http://www.dashcommerce.org

Software distributed under the License is distributed on an "AS IS"
basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
License for the specific language governing rights and limitations
under the License.

The Original Code is dashCommerce.

The Initial Developer of the Original Code is Mettle Systems LLC.
Portions created by Mettle Systems LLC are Copyright (C) 2007. All Rights Reserved.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.IO;
using System.Text;
using System.Xml;

namespace Commerce.Providers
{
    public class USPostalServiceShippingProvider : FulfillmentProvider
    {
        private static readonly Dictionary<int, IList<UspsDeliveryOption>> _serviceLookup
            = new Dictionary<int, IList<UspsDeliveryOption>>();

        static USPostalServiceShippingProvider()
        {
            // Express Mail Hold For Pickup
            _serviceLookup[2] = new[] { new UspsDeliveryOption(2, Decimal.MaxValue, Decimal.MaxValue, Decimal.MaxValue, 108, 70) };

            // Express Mail Flat-Rate Envelope Hold For Pickup
            _serviceLookup[27] = new[] { new UspsDeliveryOption(27, 12.5M, 9.5M, 0.75M, Decimal.MaxValue, 70) };

            // Express Mail
            _serviceLookup[3] = new[] { new UspsDeliveryOption(3, Decimal.MaxValue, Decimal.MaxValue, Decimal.MaxValue, 108, 70) };

            // Express Mail Flat-Rate Envelope
            _serviceLookup[13] = new[] { new UspsDeliveryOption(13, 12.5M, 9.5M, 0.75M, Decimal.MaxValue, 70) };

            // Priority Mail
            _serviceLookup[1] = new[] { new UspsDeliveryOption(1, Decimal.MaxValue, Decimal.MaxValue, Decimal.MaxValue, 108, 70) };

            // Priority Mail Flat-Rate Envelope
            _serviceLookup[16] = new[] { new UspsDeliveryOption(16, 12.5M, 9.5M, 0.75M, Decimal.MaxValue, 70) };

            // Priority Mail Small Flat-Rate Box
            _serviceLookup[28] = new[] { new UspsDeliveryOption(28, 5.375M, 8.625M, 1.675M, Decimal.MaxValue, 70) };

            // Priority Mail Regular/Medium Flat-Rate Boxes
            _serviceLookup[17] = new[]
                                    {
                                        new UspsDeliveryOption(17, 11.875M, 13.625M, 3.375M, Decimal.MaxValue, 70),
                                        new UspsDeliveryOption(17, 11M, 8.5M, 5.5M, Decimal.MaxValue, 70)
                                    };

            // Priority Mail Large Flat-Rate Box
            _serviceLookup[22] = new[] { new UspsDeliveryOption(22, 12, 12, 5.5M, Decimal.MaxValue, 70) };

            // Parcel Post
            _serviceLookup[4] = new[] { new UspsDeliveryOption(4, Decimal.MaxValue, Decimal.MaxValue, Decimal.MaxValue, 108, 70) };

            // Bound Printed Matter
            _serviceLookup[5] = new[] { new UspsDeliveryOption(5, Decimal.MaxValue, Decimal.MaxValue, Decimal.MaxValue, 108, 70) };

            // Media Mail
            _serviceLookup[6] = new[] { new UspsDeliveryOption(6, Decimal.MaxValue, Decimal.MaxValue, Decimal.MaxValue, 108, 70) };

            // Library Mail
            _serviceLookup[7] = new[] { new UspsDeliveryOption(7, Decimal.MaxValue, Decimal.MaxValue, Decimal.MaxValue, 108, 70) };
        }

        #region Variables
        private string _connectionUrl = "";
        private string _uspsUserName = "";
        private string _uspsPassword = "";
        private string _uspsAdditionalHandlingCharge = "";
        private DeliveryRestrictions _deliveryRestriction = DeliveryRestrictions.None;
        private Int32[] _servicesDisabled;

        #endregion

        #region Provider specific behaviors
        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
            {
                throw new ArgumentNullException("config");
            }

            base.Initialize(name, config);

            _connectionUrl = config["connectionUrl"];

            if (String.IsNullOrEmpty(_connectionUrl))
            {
                throw new ProviderException("Empty or missing connectionUrl");
            }

            config.Remove("connectionUrl");

            _uspsUserName = config["uspsUserName"];

            if (String.IsNullOrEmpty(_uspsUserName))
            {
                throw new ProviderException("Empty or missing uspsUserName");
            }

            config.Remove("uspsUserName");

            _uspsPassword = config["uspsPassword"];

            if (String.IsNullOrEmpty(_uspsPassword))
            {
                throw new ProviderException("Empty or missing uspsPassword");
            }

            config.Remove("uspsPassword");

            _uspsAdditionalHandlingCharge = config["uspsAdditionalHandlingCharge"];

            if (String.IsNullOrEmpty(_uspsAdditionalHandlingCharge))
            {
                throw new ProviderException("Empty or missing uspsAdditionalHandlingCharge");
            }

            config.Remove("uspsAdditionalHandlingCharge");

            String servicesDisabled = config["servicesDisabled"];

            if (!String.IsNullOrEmpty(servicesDisabled))
            {
                String[] servicesDisabledValues = servicesDisabled.Split(',');
                _servicesDisabled = new Int32[servicesDisabledValues.Length];

                for (int i = 0; i < servicesDisabledValues.Length; i++)
                {
                    Int32 serviceDisabled;
                    if (!Int32.TryParse(servicesDisabledValues[i].Trim(), out serviceDisabled))
                    {
                        throw new ProviderException("servicesDisabled attribute value is badly formatted: " +
                                                    "should be codes separated by commas.");
                    }

                    _servicesDisabled[i] = serviceDisabled;
                }

                Array.Sort(_servicesDisabled);
            }
            else
            {
                _servicesDisabled = new Int32[0];
            }

            config.Remove("servicesDisabled");

            // Throw an exception if unrecognized attributes remain
            if (config.Count > 0)
            {
                string attr = config.GetKey(0);

                if (!String.IsNullOrEmpty(attr))
                {
                    throw new ProviderException("Unrecognized attribute: " + attr);
                }
            }
        }
        #endregion

        #region Methods
        public override DeliveryOptionCollection GetDeliveryOptions(PackageInfo package)
        {
            HttpRequestHandler http = new HttpRequestHandler(_connectionUrl);
            string rateXml = http.Post(RateRequest(package));
            DeliveryOptionCollection collection = UspsParseRates(rateXml, package);
            return collection;
        }

        public override DeliveryOptionCollection GetDeliveryOptions(PackageInfo package, DeliveryRestrictions restrictions)
        {
            // TODO: I need to put a little more thought into the restrictions
            if (restrictions == DeliveryRestrictions.Download)
            {
                throw new Exception("Shipping Error: This item is download only.");
            }

            _deliveryRestriction = restrictions;
            HttpRequestHandler http = new HttpRequestHandler(_connectionUrl);
            string rateXml = http.Post(RateRequest(package));
            DeliveryOptionCollection collection = UspsParseRates(rateXml, package);
            return collection;
        }

        public override DeliveryOptionCollection GetDeliveryOptions(PackageInfo package, decimal orderAmount, int orderQuantity)
        {
            HttpRequestHandler http = new HttpRequestHandler(_connectionUrl);
            string rateXml = http.Post(RateRequest(package));
            DeliveryOptionCollection collection = UspsParseRates(rateXml, package);
            return collection;
        }

        private bool IsPackageTooLarge(int length, int height, int width)
        {
            int total = TotalPackageSize(length, height, width);
            return total > 130;
        }

        private int TotalPackageSize(int length, int height, int width)
        {
            int girth = height + height + width + width;
            int total = girth + length;
            return total;
        }

        private bool IsPackageTooHeavy(int weight)
        {
            return weight > 70;
        }

        private string GetPackageSize(int length, int height, int width)
        {
            int girth = height + height + width + width;
            int total = girth + length;

            if (total <= 84)
            {
                return "REGULAR";
            }

            if ((total > 84) && (total <= 108))
            {
                return "LARGE";
            }

            if ((total > 108) && (total <= 130))
            {
                return "OVERSIZE";
            }

            throw new Exception("Shipping Error: Package too large.");
        }

        private DeliveryOptionCollection UspsParseRates(string response, PackageInfo package)
        {
            DeliveryOptionCollection optionCollection = new DeliveryOptionCollection();

            using (XmlTextReader reader = new XmlTextReader(new StringReader(response)))
            {
                while (reader.Read())
                {
                    if ((reader.Name == "Error") && (reader.NodeType == XmlNodeType.Element))
                    {
                        string errorText = "";

                        while (reader.Read())
                        {
                            if ((reader.Name == "HelpContext") && (reader.NodeType == XmlNodeType.Element))
                            {
                                errorText += "USPS Help Context: " + reader.ReadString() + ", ";
                            }

                            if ((reader.Name == "Description") && (reader.NodeType == XmlNodeType.Element))
                            {
                                errorText += "Error Desc: " + reader.ReadString();
                            }
                        }

                        throw new ProviderException("USPS Error returned: " + errorText);
                    }

                    if ((reader.Name == "Postage") && (reader.NodeType == XmlNodeType.Element))
                    {
                        int serviceCode;
                        Int32.TryParse(reader.GetAttribute("CLASSID"), out serviceCode);
                        string postalRate = "";
                        String mailService = "";

                        reader.Read();

                        if (reader.Name == "MailService" && reader.NodeType == XmlNodeType.Element)
                        {
                            mailService = reader.ReadString();
                            reader.ReadEndElement();
                        }

                        if (reader.Name == "Rate" && reader.NodeType == XmlNodeType.Element)
                        {
                            postalRate = reader.ReadString();
                            reader.ReadEndElement();
                        }

                        IList<UspsDeliveryOption> options = GetDeliveryOptions(serviceCode, package);

                        if (options == null)
                        {
                            continue;
                        }

                        decimal rate;

                        if (!Decimal.TryParse(postalRate, out rate))
                        {
                            continue;
                        }

                        if (!String.IsNullOrEmpty(_uspsAdditionalHandlingCharge))
                        {
                            decimal additionalHandling;

                            if (Decimal.TryParse(_uspsAdditionalHandlingCharge, out additionalHandling))
                            {
                                rate += additionalHandling;
                            }
                        }

                        // Weed out unavailable service rates
                        foreach (UspsDeliveryOption option in options)
                        {
                            if (!option.Fits(package))
                            {
                                continue;
                            }

                            DeliveryOption deliveryOption = new DeliveryOption();
                            deliveryOption.Rate = rate;
                            deliveryOption.Service = mailService;
                            optionCollection.Add(deliveryOption);

                            // Found at least one option in this class
                            break;
                        }
                    }
                }

                return optionCollection;
            }
        }

        private IList<UspsDeliveryOption> GetDeliveryOptions(int serviceCode, PackageInfo package)
        {
            IList<UspsDeliveryOption> options;

            if (!IsServiceEnabled(serviceCode))
            {
                return null;
            }

            if (!_serviceLookup.TryGetValue(serviceCode, out options))
            {
                return null;
            }

            if (IsMediaService(serviceCode))
            {
                String isMediaValue;
                Boolean isMedia;

                if (package.Args == null
                    || !package.Args.TryGetValue(PackageInfoArgs.IsMedia, out isMediaValue)
                    || !Boolean.TryParse(isMediaValue, out isMedia)
                    || !isMedia)
                {
                    return null;
                }   
            }

            return options;
        }

        private bool IsServiceEnabled(int serviceCode)
        {
            return Array.BinarySearch(_servicesDisabled, serviceCode) < 0;
        }

        private static bool IsMediaService(int serviceCode)
        {
            return serviceCode == 6;
        }

        // Create rating request XML string
        private string RateRequest(PackageInfo package)
        {
            // Changed weight logic per JeffreyABecker suggestions
            int tOz = (int)Math.Ceiling(package.Weight * 16.0m);
            int packageWeightPounds = tOz / 16;
            int packageWeightOunces = tOz % 16;
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("<RateV3Request USERID=\"{0}\" PASSWORD=\"{1}\">", _uspsUserName, _uspsPassword);
            string packageStr = BuildRatePackage(package,
                                                 packageWeightPounds,
                                                 packageWeightOunces,
                                                 package.Length,
                                                 package.Height,
                                                 package.Width);
            sb.Append(packageStr);
            sb.Append("</RateV3Request>");
            return "API=RateV3&XML=" + sb;
        }

        private string BuildRatePackage(PackageInfo package,
                                        int packageWeightPounds,
                                        int packageWeightOunces,
                                        int length,
                                        int height,
                                        int width)
        {
            if (!IsPackageTooHeavy(packageWeightPounds)
                && !IsPackageTooLarge(length, height, width))
            {
                // Rate single package
                string packageSize = GetPackageSize(length, height, width);
                StringBuilder sb = new StringBuilder();
                sb.Append("<Package ID=\"0\">");
                sb.Append("<Service>ALL</Service>");
                sb.AppendFormat("<ZipOrigination>{0}</ZipOrigination>", package.FromZip);
                sb.AppendFormat("<ZipDestination>{0}</ZipDestination>", package.ToZip);
                sb.AppendFormat("<Pounds>{0}</Pounds>", packageWeightPounds);
                sb.AppendFormat("<Ounces>{0}</Ounces>", packageWeightOunces);
                sb.AppendFormat("<Size>{0}</Size>", packageSize);
                sb.Append("<Machinable>FALSE</Machinable>"); // Packages are not machinable
                sb.Append("</Package>");
                return sb.ToString();
            }
            else
            {
                // Rate multiple packages
                int totalPackages = TotalPackageSize(length, height, width) / 108;
                int tempPackageWeightPounds = packageWeightPounds / totalPackages;
                int temppackageWeightOunces = packageWeightOunces / totalPackages;
                int tempPackageHeight = height / totalPackages;
                int tempPackageWidth = width / totalPackages;
                string packageSize = GetPackageSize(length, tempPackageHeight, tempPackageWidth);
                StringBuilder sb = new StringBuilder();

                for (int i = 0; i <= totalPackages; i++)
                {
                    sb.AppendFormat("<Package ID=\"{0}\">", i);
                    sb.Append("<Service>ALL</Service>");
                    sb.AppendFormat("<ZipOrigination>{0}</ZipOrigination>", package.FromZip);
                    sb.AppendFormat("<ZipDestination>{0}</ZipDestination>", package.ToZip);
                    sb.AppendFormat("<Pounds>{0}</Pounds>", tempPackageWeightPounds);
                    sb.AppendFormat("<Ounces>{0}</Ounces>", temppackageWeightOunces);
                    sb.AppendFormat("<Size>{0}</Size>", packageSize);
                    sb.Append("<Machinable>FALSE</Machinable>"); // Packages are not machinable
                    sb.Append("</Package>");
                }

                return sb.ToString();
            }
        }
        #endregion
    }

    internal class UspsDeliveryOption
    {
        public UspsDeliveryOption(int classId,
                                  Decimal widthLimit,
                                  Decimal heightLimit,
                                  Decimal lengthLimit,
                                  Decimal combinedLimit,
                                  Decimal weightLimit)
        {
            ClassId = classId;
            WidthLimit = widthLimit;
            HeightLimit = heightLimit;
            LengthLimit = lengthLimit;
            CombinedLimit = combinedLimit;
            WeightLimit = weightLimit;
        }

        public int ClassId { get; private set; }
        public Decimal WidthLimit { get; private set; }
        public Decimal HeightLimit { get; private set; }
        public Decimal LengthLimit { get; private set; }
        public Decimal CombinedLimit { get; private set; }
        public Decimal WeightLimit { get; private set; }

        public bool Fits(PackageInfo package)
        {
            if (WeightLimit < package.Weight)
            {
                return false;
            }

            if (Math.Min(HeightLimit, WidthLimit) < Math.Min(package.Width, package.Height))
            {
                return false;
            }

            if (LengthLimit < package.Length)
            {
                return false;
            }

            if (CombinedLimit < package.Width + package.Height)
            {
                return false;
            }

            return true;
        }
    }
}
